#ifndef __INI_H__
  #define __INI_H__

#include <vector>
#include <fstream>
#include <string>

#include "exceptions.h"
#include "ini_category.h"

using namespace std;

//! A class which represents an ini file

/*!
This class is designed for holding & parsing ini files.

Implementation date: 2008

Usage:

\code
string str_tmp;
ini i;
i.from_file("my_ini_file.ini");
cout << i.get_value("cat1", "key1", str_tmp) << endl;
\endcode
*/

class ini {
private:
  vector<ini_category> categories;
  char *buffer;

  //! assignment operator: not allowed
  ini &operator=(const ini &i);

  //! copy constructor: not allowed
  ini(const ini &i);

  void release_buffer() {
    delete[] buffer;
    buffer = 0;
  }

  //! reads the whole file and copies its content to the buffer (may throw an exception)
  void full_read_file(const char *file_name) {
    size_t count = 0;
    FILE *f = fopen(file_name, "rb");
    if (f == 0) {
      string msg = "Ini file '%1' not found!";
      str::format(msg, arg<const char*>(file_name));
      throw ini_exception(msg);
    } // if

    if (fseek(f, 0, SEEK_END) == 0) { /* go to the end of the file */
      const long len = ftell(f);

      release_buffer();
      buffer = new char[len + 1];

      if (fseek(f, 0, SEEK_SET) == 0) { /* go to the begining of the file */
        count = fread(buffer, sizeof(char), len, f);
        buffer[len] = '\0';

        if (fclose(f) == EOF)
          throw ini_exception("Ini file could not be closed successfully!");

        if (count == 0)
          throw ini_exception("Read error!");

      } // if
    } // if
  }

public:
  //! clears all categories of this object
  void clear() {
    categories.clear();
  }

  //! returns the number of categories
  size_t get_categories_count() const {
    return categories.size();
  }

  //! adds a new category (may throw an exception)
  void add_category(const ini_category &ic) {
    const size_t len = categories.size();
    const string &cat_name = ic.get_category_name();
    for (size_t i = 0; i < len; ++i) {
      if (categories[i].get_category_name() == cat_name) {
        string msg = "Category '%1' is not unique!";
        str::format(msg, arg<string>(cat_name));
        throw ini_exception(msg);
      } // if
    } // for i

    categories.push_back(ic);
  }

  //! removes the given category and all of its items from the ini. if the category name does not exist, then this function will return false
  bool remove_category(const string &category_name) {
    for (vector<ini_category>::iterator it = categories.begin(); it != categories.end(); ++it) {
      if (category_name == it->get_category_name()) {
        categories.erase(it);
        return true;
      } // if
    } // for it

    return false;
  }

  //! returns the value of the given key in the given category or returns an empty string if it is not found
  string &get_value(const string &category_name, const string &key, string &output) const {
    output.resize(0);
    const size_t len = categories.size();
    for (size_t i = 0; i < len; ++i) {
      if (categories[i].get_category_name() == category_name) {
        output = categories[i].get_value(key);
        return output;
      } // if
    } // for i

    return output;
  }

  //! returns the string representation of this object
  string &to_string(string &output) const {
    output.resize(0);
    string tmp_str;

    const size_t len = categories.size();
    for (size_t i = 0; i < len; ++i)
      output += categories[i].to_string(tmp_str);

    return output;
  }

  //! writes the data to an ini file (may throw an exception)
  void to_file(const string &file_name) const {
    string tmp_str;
    to_string(tmp_str);

    ofstream out(file_name.c_str());
    if (out.good() == true)
      out << tmp_str;
    else
      throw ini_exception("Write error!");

    out.close();
  }

  //! loads the data from the given ini file (may throw an exception)
  void from_file(const string &file_name) {
    categories.resize(0);
    full_read_file(file_name.c_str());

    string tmp_str;
    ini_category ic;

    bool must_add_category = false;

    const char *tmp = strtok(this->buffer, "\r\n");
    while (tmp != 0) {
      if (tmp[0] != '#') {
        tmp_str = tmp;
        if (tmp[0] == '[') {
          if (must_add_category == true) {
            this->add_category(ic);
            ic.clear();
            must_add_category = false;
          } else
            must_add_category = true;

          ic.set_category_name(tmp_str.substr(1, tmp_str.length() - 2)); // ignores the [ ]
        } else {
          const size_t index = tmp_str.find('=');
          if (index != string::npos) {
            ic.add_item(tmp_str.substr(0, index), tmp_str.substr(index + strlen("=")));
            must_add_category = true;
          } // if
        } // else
      } // if

      tmp = strtok(0, "\r\n");
    } // while

    this->add_category(ic);

    release_buffer();
  }

  //! default constructor
  ini() : buffer(0) {
    // nothing to do
  }

  //! destructor
  virtual ~ini() {
    release_buffer();
  }
};

#endif
